use std::{
    fmt::Display,
    fs::{self, File},
    io::Read,
    iter::Peekable,
    path::Path,
    vec::IntoIter,
};

use thiserror::Error;

use crate::token::{Token, lookup_ident};

#[derive(Clone, Debug)]
pub struct Location {
    pub filename: String,
    pub line: usize,
    pub column: usize,
}
impl Display for Location {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "@{}:{}:{}", self.filename, self.line, self.column)
    }
}

pub trait HasLocation {
    fn location(&self) -> Location;
}

#[derive(Debug, Error)]
pub enum LexError {
    #[error("{0}")]
    IoError(#[from] std::io::Error),
    #[error("{0}")]
    FromUtf8Error(#[from] std::string::FromUtf8Error),
}

const EOF: char = '\u{0}';

pub type Result<T> = std::result::Result<T, LexError>;

pub struct Lexer {
    input: Peekable<IntoIter<char>>,
    ch: char,
    cur_line: usize,
    cur_column: usize,
    location: Location,
}
impl Lexer {
    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
        let filename = if is_dash(&path) {
            "-".to_string()
        } else {
            fs::canonicalize(&path)?
                .into_os_string()
                .to_str()
                .unwrap()
                .to_string()
        };
        let content = read_content(path)?;
        let mut l = Self {
            input: content.chars().collect::<Vec<_>>().into_iter().peekable(),
            ch: EOF,
            cur_line: 1,
            cur_column: 0,
            location: Location {
                filename,
                line: 1,
                column: 0,
            },
        };
        l.read_char();
        Ok(l)
    }

    fn read_char(&mut self) {
        self.ch = self.input.next().unwrap_or(EOF);
        if self.ch == '\n' {
            self.cur_line += 1;
            self.cur_column = 0;
        } else {
            self.cur_column += 1;
        }
    }

    pub fn next_token(&mut self) -> Token {
        self.skip_ws_and_comment();

        self.location.line = self.cur_line;
        self.location.column = self.cur_column;

        let tok = match self.ch {
            ';' => Token::Semicolon,
            '(' => Token::LParen,
            ')' => Token::RParen,
            '{' => Token::LBrace,
            '}' => Token::RBrace,
            '[' => Token::LBracket,
            ']' => Token::RBracket,
            '+' => Token::Plus,
            '*' => Token::Asterisk,
            ',' => Token::Comma,
            '<' => Token::Lt,
            '>' => Token::Gt,
            '=' => Token::Assign,
            EOF => Token::Eof,
            ch if is_digit(ch) => return self.read_num(),
            ch if is_letter(ch) => return lookup_ident(&self.read_ident()),
            _ => Token::Illegal(format!("{}", self.ch)),
        };
        self.read_char();
        tok
    }

    fn skip_ws_and_comment(&mut self) {
        while is_whitespace(self.ch) || is_comment_start(self.ch) {
            if is_whitespace(self.ch) {
                self.read_char();
            } else {
                while self.ch != '\n' && self.ch != EOF {
                    self.read_char();
                }
            }
        }
    }

    fn read_num(&mut self) -> Token {
        let mut buf = String::new();
        while is_digit(self.ch) || self.ch == '.' {
            buf.push(self.ch);
            self.read_char();
        }
        match buf.parse::<f64>() {
            Ok(value) => Token::Num(value),
            Err(_) => Token::Illegal(buf),
        }
    }

    fn read_ident(&mut self) -> String {
        let mut buf = String::new();
        while is_letter(self.ch) || is_digit(self.ch) {
            buf.push(self.ch);
            self.read_char();
        }
        buf
    }

    pub fn location_ref(&self) -> &Location {
        &self.location
    }
}

fn is_dash<P: AsRef<Path>>(path: P) -> bool {
    path.as_ref().file_name().map_or(false, |name| name == "-")
}

pub fn read_content<P: AsRef<Path>>(path: P) -> std::result::Result<String, std::io::Error> {
    let mut reader: Box<dyn Read> = if is_dash(&path) {
        Box::new(std::io::stdin())
    } else {
        Box::new(File::open(path)?)
    };
    let mut buf = String::new();
    reader.read_to_string(&mut buf)?;
    Ok(buf)
}
fn is_letter(ch: char) -> bool {
    match ch {
        'a'..='z' | 'A'..='Z' | '_' => true,
        _ => false,
    }
}

fn is_digit(ch: char) -> bool {
    match ch {
        '0'..='9' => true,
        _ => false,
    }
}

fn is_comment_start(ch: char) -> bool {
    ch == '#'
}

fn is_whitespace(ch: char) -> bool {
    matches!(ch, ' ' | '\t' | '\r' | '\n')
}
